package com.mxx.common.data.jpa;

import com.mxx.common.conv.BeanUtil;
import com.mxx.common.data.domain.JpaThreadContext;
import com.mxx.common.data.utils.SortPaged;
import com.mxx.common.exception.CommentException;
import com.mxx.common.utils.ClassUtil;
import com.mxx.common.utils.CollectionUtil;
import com.mxx.common.utils.Strings;
import com.sun.org.apache.bcel.internal.generic.IF_ACMPEQ;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation;
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
import org.springframework.stereotype.Repository;

import javax.persistence.EntityManager;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;

/**
 * @Author itzgyw
 * @Date 2020-01-06
 * @Time 15:07
 * @Description:
 */
@Repository
public class DefaultRepositoryImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> implements DefaultRepository<T, ID> {

    private final EntityManager entityManager;
    private final Class<T> domainClass;
    private final Spec NULL_SPEC = helper -> null;
    private final String idName;
    private JpaEntityInformation<T, ID> entityInformation;
    protected static final Spec EMPTY_SPEC = helper -> helper.endAnd();
    private JpaOperationHandler operationHandler;


    public DefaultRepositoryImpl(JpaEntityInformation<T, ID> entityInformation, EntityManager entityManager) {
        super(entityInformation, entityManager);
        this.entityInformation = entityInformation;
        this.domainClass = entityInformation.getJavaType();
        this.idName = entityInformation.getIdAttribute().getName();
        this.entityManager = entityManager;
        this.operationHandler = new JpaOperationHandler<>(entityInformation, entityManager);
    }

    public final Spec<T> emptySpec() {
        return EMPTY_SPEC;
    }


    @Override
    public ID getIdValue(T entity) {
        return entityInformation.getId(entity);
    }

    @Override
    public String idName() {
        return idName;
    }

    @Override
    public Optional<T> findById(ID id) {
        if (Strings.isEmpty(id)) {
            return Optional.empty();
        } else {
            Spec<T> spec = this.operationHandler.getIdSpec(id);
            return selectFirst(spec);
        }
    }

    @Override
    public Optional findById(ID id, String... columns) {
        if (Strings.isEmpty(id)) {
            return Optional.empty();
        } else {
            Spec<T> spec = this.operationHandler.getIdSpec(id);
            List list = selectList(spec, columns);
            Object entity = CollectionUtil.first(list, null);
            return Optional.ofNullable(entity);
        }
    }

    @Override
    public Optional findById(Class voClass, ID id, String... columns) {
        if (Strings.isEmpty(id)) {
            return Optional.empty();
        } else {
            Spec<T> spec = this.operationHandler.getIdSpec(id);
            List list = selectList(voClass, spec, columns);
            Object entity = CollectionUtil.first(list, null);
            return Optional.ofNullable(entity);
        }
    }

    @Override
    public Optional<T> findOne(Specification<T> spec) {
        return super.findOne(spec);
    }

    @Override
    public Optional<T> findOneAndDetach(Specification<T> spec) {
        Optional<T> opt = super.findOne(spec);
        opt.ifPresent(entityManager::detach);
        return opt;
    }

    @Override
    public List<T> findAll() {
        return this.selectList();
    }

    @Override
    public List<T> findAll(Specification<T> spec) {
        return this.selectList(spec);
    }

    @Override
    public Page<T> findAll(Specification<T> spec, Pageable pageable) {
        return this.selectPage(spec, pageable);
    }

    @Override
    public List<T> findAll(Sort sort) {
        Spec<T> spec = helper -> helper.orderBy(sort).endAnd();
        return this.selectList(spec);
    }

    @Override
    public Page<T> findAll(Pageable pageable) {
        return this.selectPage(pageable);
    }

    @Override
    public List<T> findAll(Specification<T> spec, Sort sort) {
        Spec<T> sortSpec = helper -> helper.orderBy(sort).endAnd();
        return this.selectList(spec.and(sortSpec));
    }

    @Override
    public List<T> findAllById(Iterable<ID> ids) {
        List<ID> list = new ArrayList<>();
        ids.forEach(p -> {
            list.add(p);
        });
        Spec<T> spec = this.operationHandler.getIdSpec(list);
        return selectList(spec);
    }


    @Override
    public void saveOfSpecifiedID(T t) {
        ID id = entityInformation.getId(t);
        if (id != null) {
            JpaThreadContext.setUserId(id);
        }
        save(t);
    }

    @Override
    public void saveOfSpecifiedIDNoTx(T entity) {
        ID id = entityInformation.getId(entity);
        if (id != null) {
            JpaThreadContext.setUserId(id);
        }
        saveNoTx(entity);
    }

    @Override
    public void saveOfSpecifiedID(List<T> t) {
        for (int index = 0; index < t.size(); index++) {
            saveOfSpecifiedID(t.get(index));
        }
    }

    @Override
    public T saveNoTx(T entity) {
        if (this.entityInformation.isNew(entity)) {
            this.entityManager.persist(entity);
            return entity;
        } else {
            return this.entityManager.merge(entity);
        }
    }

    @Override
    public T saveOrUpdate(T entity) {
        ID id = entityInformation.getId(entity);
        if (Strings.isNotEmpty(id)) {
            if (!this.entityInformation.isNew(entity)) {
                return this.entityManager.merge(entity);
            } else {
                Optional<T> optionalT = findById(id);
                if (optionalT.isPresent()) {
                    T oldEntity = optionalT.get();
                    BeanUtil.replace(entity, oldEntity, idName);
                    this.saveAndFlush(oldEntity);
                    this.entityManager.detach(oldEntity);
                    BeanUtil.replace(oldEntity, entity);
                }
                return entity;
            }
        } else {
            this.entityManager.persist(entity);
            return entity;
        }
    }

    @Override
    public Optional selectFirst(Specification<T> spec, String... columns) {
        List list = selectList(spec, columns);
        return Optional.ofNullable(CollectionUtil.first(list, null));
    }

    @Override
    public Optional selectFirst(Class voClass, Specification<T> spec, String... columns) {
        List list = selectList(voClass, spec, columns);
        return Optional.ofNullable(CollectionUtil.first(list, null));
    }

    @Override
    public List selectList(String... columns) {
        return selectList(domainClass, columns);
    }

    @Override
    public List selectList(Class voClass, String... columns) {
        return selectList(voClass, NULL_SPEC, columns);
    }

    @Override
    public List selectList(Specification<T> spec, String... columns) {
        return selectList(domainClass, spec, columns);
    }

    @Override
    public List selectList(Class voClass, Specification<T> spec, String... columns) {
        return (List) this.operationHandler.baseQuery(voClass, spec, SortPaged.unSort, columns);
    }

    @Override
    public Page selectPage(Pageable pageable, String... columns) {
        return selectPage(domainClass, EMPTY_SPEC, pageable, columns);
    }

    @Override
    public Page selectPage(Class voClass, Pageable pageable, String... columns) {
        return (Page) this.operationHandler.baseQuery(voClass, EMPTY_SPEC, pageable, columns);

    }

    @Override
    public Page selectPage(Specification<T> spec, Pageable pageable, String... columns) {
        return selectPage(domainClass, spec, pageable, columns);
    }

    @Override
    public Page selectPage(Class voClass, Specification<T> spec, Pageable pageable, String... columns) {
        return (Page) this.operationHandler.baseQuery(voClass, spec, pageable, columns);
    }

    @Override
    public boolean updateById(T entity) {
        ID id = entityInformation.getId(entity);
        if (Strings.isEmpty(id)) {
            return false;
        } else {
            Optional<T> optionalT = findById(id);
            if (!optionalT.isPresent()) {
                return false;
            }
            T oldEntity = optionalT.get();
            BeanUtil.replace(entity, oldEntity, idName);
            this.saveAndFlush(oldEntity);

            this.entityManager.detach(oldEntity);
            BeanUtil.replace(oldEntity, entity);
            return true;
        }
    }

    @Override
    public int updateMulti(UpdateSpecification<T> spec) {
        return this.operationHandler.baseUpdate(spec);
    }

    @Override
    public boolean updateOnly(UpdateSpecification<T> spec) {
        int count = this.operationHandler.baseUpdate(spec);
        if (count > 1) {
            throw new CommentException("One is expected to be revised, but %s are", count);
        }
        return count == 1;
    }

    @Override
    public void deleteById(ID id) {
        Spec<T> spec = this.operationHandler.getIdSpec(id);
        if (!deleteOnly(spec)) {
            throw new CommentException("No %s entity with id %s exists!", entityInformation.getJavaType(), id);
        }
    }

    @Override
    public int deleteByIds(Collection<ID> ids) {
        Spec<T> spec = this.operationHandler.getIdSpec(ids);
        return deleteMulti(spec);
    }

    @Override
    public int deleteMulti(Specification<T> spec) {
        return this.operationHandler.baseDelete(spec);
    }

    @Override
    public boolean deleteOnly(Specification<T> spec) {
        int count = this.operationHandler.baseDelete(spec);
        if (count > 1) {
            throw new CommentException("One is expected to be revised, but %s are", count);
        }
        return count == 1;
    }

    @Override
    public void delete(T entity) {
        ID id = entityInformation.getId(entity);
        Spec<T> spec = this.operationHandler.getIdSpec(id);
        deleteOnly(spec);
    }

    @Override
    public void deleteAll() {
        deleteMulti(EMPTY_SPEC);
    }

    @Override
    public void deleteAll(Iterable<? extends T> entities) {
        List<ID> list = new ArrayList<>();
        entities.forEach(p -> {
            ID id = entityInformation.getId(p);
            list.add(id);
        });
        Spec<T> spec = this.operationHandler.getIdSpec(list);
        deleteMulti(spec);
    }

    @Override
    public void deleteInBatch(Iterable<T> entities) {
        this.deleteAll(entities);
    }

    @Override
    public void deleteAllInBatch() {
        this.deleteAll();
    }

    @Override
    public boolean exists(Specification spec) {
        return this.count(spec) > 0;
    }


    @Override
    public Class<T> getDomainClass() {
        return domainClass;
    }

}
